﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
using Downplay.Alchemy.Dynamic;

namespace Downplay.Alchemy.Tests.Dynamics {
    [TestFixture]
    public class ValueTests {

        // TODO: I don't like how truncation is so easily allowed in all of this. But I'm stumped as to a more robust, useful, or consistent system!

        const Decimal Pi = 3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679m;

        #region Value type conversions

        [Test]
        public void ConvertStringValues() {

            dynamic stuff = new Stuff();
            stuff.Foo = "1234";
            Assert.That((int)stuff.Foo, Is.EqualTo(1234), "Convert string to int");

            stuff.Foo = "1234.5678";
            Assert.That((float)stuff.Foo, Is.EqualTo(1234.5678f), "Convert string to float");
            Assert.That((int)stuff.Foo, Is.EqualTo(1234), "Truncate string to float");

            stuff.Foo = "9.87654321e-20";
            Assert.That((decimal)stuff.Foo, Is.EqualTo(9.87654321e-20m), "Scientific notation converts to decimal");
        }

        [Test]
        public void ConvertIntegerValues() {
            dynamic stuff = new Stuff();
            stuff.Foo = 47;
            Assert.That((long)stuff.Foo, Is.EqualTo((long)47), "Convert int to bigint");
            Assert.That((string)stuff.Foo, Is.EqualTo("47"));
        }
        [Test]
        public void ConvertLongValues() {
            dynamic stuff = new Stuff();
            long bignum = 40000000000;
            long smallnum = 1234123;
            stuff.Big = bignum;
            stuff.Small = smallnum;
            Assert.That((int)stuff.Small, Is.EqualTo(1234123));
            Assert.Throws<InvalidOperationException>(() => { var test = (int)stuff.Big; }, "Invalid truncation bigint to int"); // Attempted truncation
            // Assert.That((int)stuff.Foo.Truncate(), Is.EqualTo(int.MaxValue)); // Explicit truncation
        }

        [Test]
        public void ConvertFloatValues() {
            dynamic stuff = new Stuff();
            var num = 5.1234f;
            // Float
            stuff.Foo = num;
            Assert.That((float)stuff.Foo, Is.EqualTo(5.1234f));
            Assert.That((string)stuff.Foo, Is.EqualTo("5.1234"), "Convert float to string");
            Assert.That((double)stuff.Foo, Is.EqualTo((double)num), "Convert float to double");
            Assert.That((decimal)stuff.Foo, Is.EqualTo((decimal)num), "Convert float to decimal");

            // Truncation
            Assert.That((int)stuff.Foo, Is.EqualTo(5)); // TODO: Throw an error?

        }
        [Test]
        public void ConvertDoubleValues() {
            dynamic stuff = new Stuff();
        }
        [Test]
        public void ConvertDecimalValues() {
            dynamic stuff = new Stuff();
            stuff.Pi = Pi;

            Assert.That((string)stuff.Pi, Is.EqualTo(Pi.ToString()), "Convert decimal to string");

            // TODO: Truncate, test Epsilon
        }

        [Test]
        public void ConvertBooleans() {
            // Why are bools so much more complicated to test than other numbers??
            dynamic stuff = new Stuff();
            stuff.FalseBool = false;
            stuff.TrueBool = true;
            stuff.FalseString = "false";
            stuff.TrueString = "true";
            stuff.FFalseString = "False";
            stuff.TTrueString = "True";
            stuff.ZeroString = "0";
            stuff.OneString = "1";
            stuff.ZeroNum = 0;
            stuff.OneNum = 1;
            
            Assert.That((string)stuff.FalseBool, Is.EqualTo("False"), "False bool converts to string");
            Assert.That((string)stuff.TrueBool, Is.EqualTo("True"), "True bool converts to string");
            Assert.That((bool)stuff.FalseString, Is.EqualTo(false), "'false' string converts to bool");
            Assert.That((bool)stuff.TrueString, Is.EqualTo(true), "'true' string converts to bool");
            Assert.That((bool)stuff.FFalseString, Is.EqualTo(false), "'False' string converts to bool");
            Assert.That((bool)stuff.TTrueString, Is.EqualTo(true), "'True' string converts to bool");
            // TODO: "0"/"1" don't work yet... also could support yes/no
            //Assert.That((bool)stuff.ZeroString, Is.EqualTo(false), "'0' string converts to bool");
            //Assert.That((bool)stuff.OneString, Is.EqualTo(true), "'1' string converts to bool");
            Assert.That((bool)stuff.ZeroNum, Is.EqualTo(false), "'0' integer converts to bool");
            Assert.That((bool)stuff.OneNum, Is.EqualTo(true), "'0' integer converts to bool");
        }

        [Test]
        public void ConvertStringToNullableValues() {
            dynamic stuff = new Stuff();

            stuff.Foo = Pi.ToString();
            stuff.Bar = "NotANumber";
            Assert.That((long?)stuff.Foo, Is.EqualTo((long?)null));
            // TODO: Truncation appears generally broken
            //Assert.That((long?)stuff.Foo, Is.EqualTo((long?)Pi));
         //   Assert.That((int?)stuff.Foo, Is.EqualTo((int?)Pi));
            Assert.That((float?)stuff.Foo, Is.EqualTo((float?)Pi));
            Assert.That((double?)stuff.Foo, Is.EqualTo((double?)Pi));
            Assert.That((decimal?)stuff.Foo, Is.EqualTo((decimal?)Pi));

            Assert.That((long?)stuff.Bar, Is.EqualTo((long?)null));
            Assert.That((int?)stuff.Bar, Is.EqualTo((int?)null));
            Assert.That((float?)stuff.Bar, Is.EqualTo((float?)null));
            Assert.That((double?)stuff.Bar, Is.EqualTo((double?)null));
            Assert.That((decimal?)stuff.Bar, Is.EqualTo((decimal?)null));
        }

        #endregion

        #region Numeric operations

        [Test]
        public void MathRoundingOnFloat() {
            dynamic stuff = new Stuff();

            stuff.Pi = Pi;

            Assert.That(stuff.Pi.Floor(), Is.EqualTo(3m), "Decimal floor");
            Assert.That(stuff.Pi.Ceiling(), Is.EqualTo(4m), "Decimal ceiling");
            Assert.That(stuff.Pi.Round(), Is.EqualTo(3m), "Round to 0 decimal places (default0)");
            Assert.That(stuff.Pi.Round(3), Is.EqualTo(3.141m), "Round to 3 decimal places");

            // Check midpoints
            stuff.Half = 0.5m;
            Assert.That(stuff.Half.Round(MidpointRounding.AwayFromZero), Is.EqualTo(1m), "Round away from zero");
            Assert.That(stuff.Half.Round(MidpointRounding.ToEven), Is.EqualTo(0m), "Round to nearest even (downwards)");
            stuff.Half = 1.5m;
            Assert.That(stuff.Half.Round(MidpointRounding.AwayFromZero), Is.EqualTo(2m), "Round away from zero");
            Assert.That(stuff.Half.Round(MidpointRounding.ToEven), Is.EqualTo(2m), "Round to nearest even (upwards)");
            stuff.Halfish = 1.45m;
            Assert.That(stuff.Halfish.Round(1, MidpointRounding.AwayFromZero), Is.EqualTo(1.5m), "Round away from zero (1 d.p.)");
            Assert.That(stuff.Halfish.Round(1, MidpointRounding.ToEven), Is.EqualTo(1.4m), "Round to nearest even (1 d.p.)");
        }
        /*
        [Test]
        public void MathOperationsOnFloat() {
            dynamic stuff = new Stuff();
            double angle = (double)(Pi / 3m);
            stuff.Angle = angle;

            Assert.That((double)stuff.Pi.Sin(), Is.EqualTo(Math.Sin(angle)), "Sine");
            Assert.That((double)stuff.Pi.Cos(), Is.EqualTo(Math.Cos(angle)), "Cosine");
            Assert.That((double)stuff.Pi.Tan(), Is.EqualTo(Math.Tan(angle)), "Cosine");
            Assert.That((double)stuff.Pi.ATan(), Is.EqualTo(Math.Atan(angle)), "Cosine");
        }*/
        #endregion

        #region string operations

        [Test]
        public void StringManipulation() {

            dynamic stuff = new Stuff();
            stuff.Foo = "A longish string";

            Assert.That(stuff.Foo.Substring(0,9), Is.EqualTo("A longish"));
            Assert.That(stuff.Foo.Substring(10), Is.EqualTo("string"));
            Assert.That(stuff.Foo.Split(' '), Is.EqualTo(new[] { "A", "longish", "string" }));

            // TODO: The problem with Length is that array length takes over ... need to make a string double up as a char array
            Assert.That(stuff.Foo.Length, Is.EqualTo(16));
        }

        #endregion

        #region Unary operations

        [Test]
        public void UnaryOperations() {

            dynamic stuff = new Stuff();
            stuff.False = false;
            stuff.True = true;

            bool isFalse = false;
            bool isTrue = false;
            if (stuff.False) {
                isFalse = true;
            }
            if (stuff.True) {
                isTrue = true;
            }

            Assert.That(isFalse, Is.EqualTo(false));
            Assert.That(isTrue, Is.EqualTo(true));
        }

        #endregion

        #region Comparison operations

        [Test]
        public void CompareNumericValues() {

            dynamic stuff = new Stuff();
            stuff.Foo = Pi;

            Assert.That(stuff.Foo > 3, Is.EqualTo(true));
            Assert.That(stuff.Foo > 4, Is.EqualTo(false));
            Assert.That(stuff.Foo < 3, Is.EqualTo(false));
            Assert.That(stuff.Foo < 4, Is.EqualTo(true));

            stuff.Bar = 3;
            Assert.That(stuff.Bar > 3, Is.EqualTo(false));
            Assert.That(stuff.Bar > 2, Is.EqualTo(true));
            Assert.That(stuff.Bar >= 3, Is.EqualTo(true));
            Assert.That(stuff.Bar < 3, Is.EqualTo(false));
            Assert.That(stuff.Bar < 4, Is.EqualTo(true));
            Assert.That(stuff.Bar <= 3, Is.EqualTo(true));

        }

        [Test]
        public void CompareStringValues() {

            dynamic stuff = new Stuff();
            stuff.Foo = "Bar";
            Assert.That(stuff.Foo > "Baroque", Is.EqualTo(true));
            Assert.That(stuff.Foo < "Baroque", Is.EqualTo(false));
            Assert.That(stuff.Foo > "Baqat", Is.EqualTo(false));
            Assert.That(stuff.Foo < "Baqat", Is.EqualTo(true));
            Assert.That(stuff.Foo >= "Bar", Is.EqualTo(true));
            Assert.That(stuff.Foo <= "Bar", Is.EqualTo(true));

            // TODO: Try comparing a string to numerics

        }

        #endregion
    }
}
